package com.maimao.kill.service;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import com.maimao.core.constant.KillConstant;
import com.maimao.kill.rabbit.RabbitService;
import com.maimao.kill.utils.KillQueue;
import com.maimao.model.kill.KillGoods;
import com.maimao.model.kill.dto.KillDto;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.maimao.kill.utils.KillQueue.UN_INIT_STOCK;

/**
 * @author MaoLin Wang
 * @date 2021/4/13 11:38 下午
 */
@Slf4j
@Service
public class KillService {
    public static final Map<Long, Boolean> stockMap = Maps.newConcurrentMap();
    public static String REDIS_STOCK_LUA = "";

    @Autowired
    private KillGoodsService killGoodsService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private KillQueue killQueue;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitService rabbitService;

    public static String STOCK_LUA_INCR = "";

    static {

        /**
         * @return >0:库存值
         *         -1：不限制
         *         -2:库存不足
         *         -3:未初始化库存
         */
        StringBuilder stockSb = new StringBuilder();
        stockSb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        stockSb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        stockSb.append("    local num = tonumber(ARGV[1]);");
        stockSb.append("    if (stock == -1) then");
        stockSb.append("        return -1;");
        stockSb.append("    end;");
        stockSb.append("    if (stock >= num) then");
        stockSb.append("        return redis.call('incrby', KEYS[1], 0 - num);");
        stockSb.append("    end;");
        stockSb.append("    return -2;");
        stockSb.append("end;");
        stockSb.append("return -3;");
        REDIS_STOCK_LUA = stockSb.toString();
        log.info("init stock LUA script success:\n{}", REDIS_STOCK_LUA);

        StringBuilder incrSb = new StringBuilder();
        incrSb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        incrSb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        incrSb.append("    local num = tonumber(ARGV[1]);");
        incrSb.append("    if (stock >= 0) then");
        incrSb.append("        return redis.call('incrby', KEYS[1], num);");
        incrSb.append("    end;");
        incrSb.append("    return -2;");
        incrSb.append("end;");
        incrSb.append("return -3;");
        STOCK_LUA_INCR = incrSb.toString();
    }

    @PostConstruct
    public void initStock() {
        List<KillGoods> killGoodsList = killGoodsService.selectAll(false);
        if (ObjectUtil.isNotEmpty(killGoodsList)) {
            log.info("初始化秒杀库存,商品数量：{}", killGoodsList.size());
            killGoodsList.stream().forEach(killGoods -> {
                if (killGoods.getStock() != null && killGoods.getStock() > 0) {
                    stockMap.put(killGoods.getId(), true);
                    redisTemplate.opsForValue().set(KillConstant.KILL_STOCK_KEY + killGoods.getId(), killGoods.getStock());
                } else {
                    stockMap.put(killGoods.getId(), false);
                    redisTemplate.opsForValue().set(KillConstant.KILL_STOCK_KEY + killGoods.getId(), 0);

                }
            });
        }
    }

    /**
     * 秒杀成功后发送消息 直接预生成订单保存到redis，省略加入购物车、生成确认订单页再提交的过程，即秒杀成功后直接跳转到支付页进行支付
     *
     * @param killId
     * @param userId
     * @return
     */
    public boolean killByQueue(Long killId, Long userId) {
       /* Boolean hasStock = stockMap.get(killId);
        if (hasStock == null || !hasStock) {
            return false;
        }*/
        Boolean killed = redisTemplate.opsForSet().isMember(KillConstant.KILL_GOODS_USER + killId, userId);
        if (/*killed*/false) {
            log.info("user {} has killed goods,killId:{}", userId, killId);
            return false;
        }
        killQueue.enQueue(new KillDto().setKillId(killId).setUserId(userId));

        return true;
    }

    public boolean killByRedisson(Long killId, Long userId) {
        /*Boolean hasStock = stockMap.get(killId);
        if (hasStock == null || !hasStock) {
            return false;
        }*/
        Boolean killed = redisTemplate.opsForSet().isMember(KillConstant.KILL_GOODS_USER + killId, userId);
        if (/*killed*/false) {
            log.info("user {} has killed goods,killId:{}", userId, killId);
            return false;
        }
        String killStockKey = KillConstant.KILL_STOCK_KEY + killId;
        long stock = reduceStock(killStockKey, 1, REDIS_STOCK_LUA);
        if (stock == UN_INIT_STOCK) {
            //未初始化
            RLock lock = redissonClient.getLock("kill_lock");
            try {
                lock.lock(2, TimeUnit.SECONDS);
                stock = reduceStock(killStockKey, 1, REDIS_STOCK_LUA);
                if (stock == UN_INIT_STOCK) {
                    KillGoods details = killGoodsService.details(killId);
                    redisTemplate.opsForValue().set(killStockKey, details == null ? 0 : details.getStock());
                    stock = reduceStock(killStockKey, 1, REDIS_STOCK_LUA);
                }
            } catch (Exception e) {
                log.error("init stock error :{}", killId);
            } finally {
                lock.unlock();
            }
        }
        if (stock >= 0) {
            //秒杀成功
            /*if (stock == 0) {
                stockMap.put(killId, false);
            }*/
            try {
                redisTemplate.opsForSet().add(KillConstant.KILL_GOODS_USER + killId, userId);
                rabbitService.sendKillSuccessMessage(new KillDto().setUserId(userId).setKillId(killId));
                log.info("秒杀成功{}-{}", killId, userId);
                return true;
            } catch (Exception e) {
                log.error("kill fail {}-{} , ex:{}", killId, userId, e);
                killFail(killId, userId);
                return false;
            }
        } else {
            /*if (stock==-2){
                stockMap.put(killId, false);
            }*/
            log.error("秒杀失败{}-{}", killId, userId);
            return false;

        }

    }

    /**
     * 秒杀失败处理
     *
     * @param killId
     * @param userId
     */
    public void killFail(Long killId, Long userId) {
        String killStockKey = KillConstant.KILL_STOCK_KEY + killId;

        try {
            //库存补偿
            long stock = reduceStock(killStockKey, -1, STOCK_LUA_INCR);
            if (stock > 0) {
                log.info("incr stock success, killId:{}", killId);
                Boolean enough = stockMap.get(killId);
                if (enough!=null&&!enough){
                    stockMap.put(killId,true);
                }
            }

            //清除已秒杀记录
            redisTemplate.opsForSet().remove(KillConstant.KILL_GOODS_USER + killId, userId);

        } catch (Exception e) {
            log.error("秒杀补偿出错:{}", e);
        }
    }

    /**
     * 减少库存
     *
     * @param key
     * @param num
     * @return
     */
    public Long reduceStock(String key, Integer num, String script) {
        List<String> keys = new ArrayList<>();
        keys.add(key);
        // 脚本里的ARGV参数
        List<String> args = new ArrayList<>();
        args.add(Integer.toString(num));
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);

        Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), num);
        return result;
    }

    public static void main(String[] args) {
        StringBuilder stockSb = new StringBuilder();
        stockSb.append("if (redis.call('exists', KEYS[1]) == 1) then");
        stockSb.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
        stockSb.append("    local num = tonumber(ARGV[1]);");
        stockSb.append("    if (stock == -1) then");
        stockSb.append("        return -1;");
        stockSb.append("    end;");
        stockSb.append("    if (stock >= num) then");
        stockSb.append("        return redis.call('incrby', KEYS[1], 0 - num);");
        stockSb.append("    end;");
        stockSb.append("    return -2;");
        stockSb.append("end;");
        stockSb.append("return -3;");
        REDIS_STOCK_LUA = stockSb.toString();
        System.out.println(REDIS_STOCK_LUA);
    }
}
